2장. 편지 자동완성 만들기

출처: 『가상 면접 사례로 배우는 생성형 AI 시스템 설계』(알리 아미니안·알렉스 쉬 지음) | 참고: aliaminian.com/books

코드는 분위기만 — tiktoken, num_beams, LoRA 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.

이 장은 0장에서 배운 척추(다음 토큰 예측·7단계 설계)를 진짜 면접 문제 하나에 끝까지 적용해 본다.

문제는 이것이다.

"지메일처럼, 사용자가 글을 쓰면 다음 구절을 슬쩍 제안해 주는 기능을 설계하라."

화면 오른쪽에 회색 글씨로 "Hope you are ___" 하고 떠오르는, 그 편지 자동완성을 만든다.


0. 이 장의 새 단어 (0장에 없는 용어만 3종)

0장 용어집에 있는 말(토큰·생성형 모델·사전 학습·미세 조정·환각 등)은 다시 풀지 않는다.

이 장에서 처음 나오는 말만 여기 셋을 먼저 적어 둔다.

막히면 이 절로 돌아오면 된다.


디코더 전용 모델 (decoder-only)

한 문장 뜻 — "다음에 올 말 맞히기"만 잘하도록 만든, 글을 지어내는 쪽에 특화된 모델 종류.

일상비유 — 끝말잇기 전용 선수. 받아 적기나 번역은 안 하고, 오직 다음 말 이어 붙이기만 한다.

한 줄 예 —

# 글을 새로 짓는 일에는 이 종류를 쓴다
AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-3B")

BPE 토큰화 (BPE tokenization)

한 문장 뜻 — 글을 자주 붙어 다니는 조각 단위로 잘라 토큰을 만드는, 요즘 표준 자르기 방식.

일상비유 — 레고 설명서. 자주 쓰는 모양은 큰 블록 한 개로, 드문 모양은 작은 블록 여러 개로 표현한다.

한 줄 예 —

# "unhappily" → 의미 조각 셋으로 잘림
enc.encode("unhappily")  # → 'un' + 'happ' + 'ily'

빔 검색 (beam search)

한 문장 뜻 — 다음 말을 고를 때 후보 여러 갈래를 동시에 들고 가다가, 끝까지 가장 그럴듯한 길 하나를 고르는 방법.

일상비유 — 미로 탈출. 갈림길에서 한 길만 찍지 않고 유망한 몇 갈래를 같이 걸어 보다가, 제일 나은 길을 택한다.

한 줄 예 —

# 후보 5갈래를 동시에 추적하다 제일 좋은 걸 고름
model.generate(num_beams=5)

(귀납 도입) 이런 적 있죠?

면접관이 말한다. "지메일 편지 자동완성, 설계해 보세요."

당신은 바로 입을 연다. "트랜스포머 쓰겠습니다. 빔 검색으로..."

면접관이 묻는다. "응답은 몇 ms 안에 떠야 하죠? 동시 사용자는요? 욕설이 제안되면요?"

말문이 막힌다.

막힌 이유 — 풀기부터 했기 때문이다. 0장 척추 4("바로 풀지 말고 먼저 묻기")를 건너뛰었다.

이 장은 그 막힘을 7단계 순서로 푼다.

이 장에서 딱 5가지만

  1. 먼저 묻는다 — 속도·규모·안전 같은 범위를 되물어 문제를 좁힌다.
  2. 문제는 "다음 토큰 예측" — 글 짓기엔 디코더 전용 모델을 고른다.
  3. 데이터는 자르고 씻는다 — BPE로 토큰화하고, 개인정보는 가린다.
  4. 모델은 받아서 살짝 더 가르친다 — 사전 학습은 다운로드, 미세 조정만 우리가.
  5. 모델 혼자선 시스템이 아니다 — 언제 제안할지(트리거)·나쁜 출력 거르기(후처리)까지 묶는다.

1. 먼저 묻기 — 요구사항 구체화

망가지는 장면

"파스타 주세요"에 바로 토마토 파스타를 내갔더니, 손님은 크림을 원했다.

면접에서 "자동완성 만들어 보라" 듣고 바로 모델 이름을 대면, 엉뚱한 걸 만들 위험이 크다.

일상비유

주문 받기. 매운맛? 사이즈? 몇 인분? 먼저 묻는다.

질문으로 범위를 좁히는 행동 자체가 점수다. 바로 설계하는 게 아니다.

비유 코드 위험
주문 전 되묻기 질문 = {"속도": "?", "안전": "?"} 안 물으면 범위 어긋남
바로 메뉴 결정 "트랜스포머 쓰겠습니다" # 질문 0 잘못된 가정 위에 전부 쌓임

한 문장 정의 — 설계의 첫걸음은 풀이가 아니라 질문이며, 속도·규모·안전·개인화 같은 범위를 되물어 모호함을 먼저 걷어 내는 것이다.

예시 폭격

(1) 완성 예 — 자동완성 문제에서 되물을 4가지.

요구사항 = {
    "응답 시간": "100ms 안에 떠야 함",   # 늦으면 글자 친 뒤에 떠서 무용
    "동시 사용자": "아주 많음",          # 규모에 따라 설계가 달라짐
    "안전성": "욕설·편향 제안 금지",     # 나쁜 제안은 신뢰를 깸
    "개인화": "지금은 미포함",           # 범위를 일부러 줄임
}

(2) 부분 완성 — 빈칸을 채워 보자. 음성 비서를 설계한다면 무엇을 먼저 물을까?

요구사항 = {
    "응답 시간": "____",   # 힌트: 사람이 답을 기다려 주는 시간
    "지원 언어": "____",   # 힌트: 한국어만? 영어도?
}

(3) 독립 적용 — "사진 한 줄 설명 생성" 기능이라면, 되물을 항목 3개를 직접 적어 보라. (예: 길이 제한? 부적절 이미지 처리? 언어?)


2. 문제 정의 — "다음 토큰 예측"과 모델 고르기

망가지는 장면

"챗봇 만들어라"에 분류 모델을 골랐다가 막혔다.

분류기는 보기 중에 고를 뿐, 새 문장을 짓지 못한다. 글 짓기엔 종류부터 다른 모델이 필요하다.

일상비유

요리사를 뽑는데, 손님은 새 요리를 원한다.

레시피 감별사(분류기)는 "이 요리 맛있다/없다"만 본다. 요리사(생성형, 디코더 전용)는 새 요리를 만든다. 자동완성은 요리사의 일이다.

비유 코드 위험
감별사 (고르기) bert로 "긍정/부정" 분류 새 글을 못 지음
끝말잇기 선수 (디코더 전용) AutoModelForCausalLM.from_pretrained(...) 지어내다 틀린 말도 함(→환각)

한 문장 정의 — 글을 새로 짓는 문제는 "다음 토큰 예측"으로 풀며, 이 일에는 디코더 전용 모델을 고른다.

왜 트랜스포머인가 (옛 방식과 비교)

옛날 모델(RNN)은 토큰을 한 줄로 세워 하나씩 처리했다. 글이 길어지면 앞 내용을 잊었다.

트랜스포머는 어텐션으로 모든 토큰을 한 번에 보며 멀리 떨어진 단어끼리도 연결한다.

비유 코드 위험
한 명씩 줄 세워 전달 (RNN) 한 토큰 처리하고 다음 토큰 길어지면 앞 내용을 잊음
모두 한 자리에서 회의 (트랜스포머) 모든 토큰 동시에 어텐션 요즘 거의 다 이 방식

예시 폭격

(1) 완성 예 — 작업에 맞는 모델 종류 고르기.

# 글 짓기(자동완성) → 디코더 전용
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-3B")

(2) 부분 완성 — 빈칸을 채워 보자. 어느 작업이 디코더 전용일까?

작업 = {
    "리뷰를 긍정/부정으로 분류": "고르기 → 디코더 전용 아님",
    "이메일 다음 문장 제안":     "____",   # 짓기? 고르기?
}

(3) 독립 적용 — "사용자 질문에 답하는 챗봇"은 짓기일까 고르기일까? 어느 모델 종류를 고를지 한 단어로 답해 보라.


3. 데이터 준비 — 씻고 자르기

망가지는 장면

이메일 원본을 그대로 모델에 넣었더니, 학습 데이터에 전화번호·이메일 주소가 그대로 섞여 들어갔다.

씻지 않은 재료를 그대로 요리에 넣은 셈이다. 개인정보가 모델에 새어 든다.

일상비유

식당 주방. 들어온 재료를 씻고(개인정보 가리기) 알맞게 썬다(토큰화).

씻기 = 전화번호·주소 같은 민감 정보를 [가림]으로 바꾸기.

썰기 = 글을 토큰 조각으로 자르기.

비유 코드 위험
재료 씻기 (정제) text.replace(전화번호, "[가림]") 안 씻으면 개인정보 유출
알맞게 썰기 (토큰화) enc.encode("Let's go") 너무 잘게/크게 썰면 비효율

한 문장 정의 — 데이터 준비는 민감 정보를 가려 씻고, 글을 BPE로 토큰 조각으로 잘라 모델이 먹기 좋게 만드는 일이다.

왜 BPE로 자르나 — 세 가지 자르기 비교

자르는 단위 비유 위험
글자 하나씩 쌀알처럼 너무 잘게 의미가 사라짐
단어 통째로 통나무처럼 너무 크게 처음 보는 단어를 못 다룸
BPE (조각) 적당한 레고 블록 균형이 좋아 요즘 표준

예시 폭격

(1) 완성 예 — BPE로 자르면 처음 보는 단어도 조각으로 처리된다.

enc.encode("unhappily")
# → 'un' + 'happ' + 'ily'  세 조각으로 잘림

(2) 부분 완성 — 빈칸을 채워 보자. 정제 함수가 무엇을 가려야 할까?

def 씻기(text):
    text = 전화번호_가리기(text)   # 010-1234-5678 → [가림]
    text = ____(text)             # 힌트: 또 다른 민감 정보 하나
    return text

(3) 독립 적용 — "Hi [email protected], call me at 010-1234-5678"을 씻으면 어떤 모양이 될지 직접 적어 보라.


4. 모델 만들기 — 받아서 살짝 더 가르치기

망가지는 장면

"우리도 모델을 처음부터 학습시키자"고 덤볐다가, GPU 비용과 시간 앞에서 멈췄다.

처음부터 학습은 거대 기업의 영역이다. 작은 팀이 따라 하면 자원이 먼저 바닥난다.

일상비유

대학까지 마친 사람을 채용해, 우리 회사 업무만 신입 연수로 짧게 가르친다.

12년 교육(사전 학습)은 남이 해 둔 걸 받고, 우리 일(미세 조정)만 더한다.

비유 코드 위험
교육 마친 사람 채용 (사전 학습 받기) snapshot_download("...Llama-3.2-3B") 처음부터 학습은 비용 폭발
신입 연수 (미세 조정) trainer.train() # 이메일 데이터만 안 하면 우리 일엔 안 맞음

한 문장 정의 — 모델은 남이 사전 학습한 것을 받아 와, 우리 과제(이메일)에만 미세 조정하는 것이 기본이다.

살짝만 가르치기 — 손잡이 일부만 만지기

미세 조정도 모든 손잡이를 다 돌리면 무겁다.

요즘은 손잡이의 1~2%만 살짝 만지는 방식(LoRA)을 쓴다. 큰 GPU 없이도 된다.

비유 코드 위험
손잡이 전부 돌리기 (전체 조정) 모든 매개변수 학습 GPU·시간 폭발
손잡이 일부만 (가벼운 조정) 1%만 학습 # LoRA 거의 같은 성능, 훨씬 가벼움

예시 폭격

(1) 완성 예 — 사전 학습된 모델을 받아 온다.

# 처음부터 안 만들고, 끝난 모델을 통째로 받음
snapshot_download("meta-llama/Llama-3.2-3B")

(2) 부분 완성 — 빈칸을 채워 보자. 미세 조정 순서다.

model = 받아오기("Llama-3.2-3B")   # 1. 사전 학습된 모델 다운로드
model = 가볍게_만들기(model)        # 2. 손잡이 1%만 학습하도록
____.train()                       # 3. 힌트: 우리 데이터로 가르치기

(3) 독립 적용 — "고객 문의 답변 봇"을 만든다면, 4단계 중 우리가 직접 하는 단계와 받아 오는 단계를 각각 골라 보라.


5. 샘플링 — 빔 검색으로 다음 구절 고르기

망가지는 장면

다음 토큰을 매번 1등 후보만 찍었더니, "the the the" 처럼 밋밋하거나 어색한 문장이 나왔다.

한 칸씩 근시안으로 고르면, 전체로 보면 더 좋은 길을 놓친다.

일상비유

미로 탈출. 갈림길마다 눈앞에 좋아 보이는 길만 찍으면 막다른 길에 갇힌다.

유망한 몇 갈래를 같이 걸어 보다가, 끝까지 가장 나은 길 하나를 택한다. 이게 빔 검색이다.

비유 코드 위험
눈앞만 보고 한 길 찍기 매번 1등 토큰만 선택 전체로 보면 어색한 문장
유망한 몇 갈래 같이 가기 (빔 검색) model.generate(num_beams=5) 후보 늘리면 조금 느려짐

한 문장 정의 — 빔 검색은 다음 말 후보 여러 갈래를 동시에 추적해, 전체로 가장 그럴듯한 완성을 고르는 샘플링 방법이다.

예시 폭격

(1) 완성 예 — 문맥을 주면 상위 제안 몇 개가 나온다.

context = "Hi Ethan! Hope you are"
smart_compose(context)
# → ["doing well.", "having a great week.", "well."]

(2) 부분 완성 — 빈칸을 채워 보자. 빔 검색 설정이다.

model.generate(
    num_beams=5,            # 후보 5갈래 동시에
    num_return_sequences=3, # 상위 ____개 제안 보여 주기
)

(3) 독립 적용 — 후보를 더 많이(num_beams=10) 들고 가면 제안 품질속도는 각각 어떻게 변할지 한 줄로 적어 보라.


6. 평가 — 시험점수와 손님반응

망가지는 장면

오프라인 시험 점수만 좋다고 안심했는데, 막상 출시하니 사용자가 제안을 안 눌렀다.

시험 점수가 높아도 손님이 안 오면 의미가 없다. 두 평가는 보는 게 다르다.

일상비유

오프라인 = 요리 대회 심사위원 점수(정해진 기준, 배포 전).

온라인 = 진짜 손님의 재방문율·수락률(실제 반응, 배포 후). 둘 다 봐야 한다.

비유 코드 위험
심사위원 점수 (오프라인) perplexity.compute(...) 점수만 보면 현장 반응을 놓침
손님 수락률 (온라인) "제안 수락률 25%" 배포 전엔 알 수 없음

한 문장 정의 — 평가는 배포 전 정해진 지표로 보는 오프라인과, 배포 후 실제 사용자 반응으로 보는 온라인 둘로 나뉘며, 한쪽만으론 충분치 않다.

예시 폭격

(1) 완성 예 — 두 종류 지표를 같이 본다.

오프라인 = "Perplexity 낮을수록 좋음"   # 배포 전
온라인  = "제안 수락률 25%"            # 배포 후 (지메일 실제 수치)

(2) 부분 완성 — 빈칸을 채워 보자. 각 지표가 어느 쪽인지.

지표 = {
    "Perplexity 점수":   "____",   # 배포 전? 후?
    "사용자 클릭 비율":  "____",
}

(3) 독립 적용 — "추천 영상 시스템"에서 오프라인 지표 한 개, 온라인 지표 한 개를 각각 떠올려 적어 보라.


7. 전체 시스템 — 모델은 부품, 시스템은 따로

망가지는 장면

좋은 모델 하나 붙였더니, 글자 하나 칠 때마다 제안이 떴다 사라지고, 가끔 욕설까지 제안됐다.

모델만으론 시스템이 아니다. 언제 제안할지 정하는 장치, 나쁜 출력을 거르는 장치가 빠진 탓이다.

일상비유

자동차. 엔진(모델)만으론 못 달린다.

시동 거는 시점을 정하는 스위치(트리거), 배기를 거르는 필터(후처리)가 있어야 진짜 차다.

비유 코드 위험
엔진 단독 (모델만) generate(input) 입력·출력이 무방비
시동 스위치 (트리거) if 단어수 >= 2: 제안() 없으면 아무 때나 깜빡임
배기 필터 (후처리) 욕설·편향 거르기 없으면 유해 제안이 그대로

한 문장 정의 — 핵심 모델은 부품 하나일 뿐이고, 언제 제안할지 정하는 트리거와 나쁜 출력을 거르는 후처리까지 묶어야 비로소 하나의 시스템이 된다.

언제 제안할지 — 트리거

매 글자마다 제안하면 화면이 정신없다.

단어가 2개 이상 쌓였고, 문장이 아직 안 끝났을 때만 제안한다.

def 제안할까(글):
    단어들 = 글.split()
    if len(단어들) < 2:           # 너무 짧으면 문맥 부족
        return False
    if 글.rstrip().endswith("."): # 문장 끝나면 새 문장 대기
        return False
    return True

나쁜 출력 거르기 — 후처리

모델이 뱉은 제안을 그대로 내보내지 않는다.

너무 길거나(8단어 초과), 편향된 표현은 거르거나 고친다.

def 거르기(제안):
    if len(제안.split()) > 8:       # 너무 긴 제안 버림
        return None
    제안 = 제안.replace("chairman", "chairperson")  # 성 중립화
    return 제안

거르기("ask the chairman for approval")
# → "ask the chairperson for approval"

예시 폭격

(1) 완성 예 — 트리거 통과 → 생성 → 후처리, 세 칸을 순서대로.

글 = "Hi Ethan, hope you"
if 제안할까(글):              # 1. 트리거: 제안할 때인가?
    후보 = smart_compose(글)  # 2. 생성: 빔 검색으로 후보
    결과 = 거르기(후보[0])     # 3. 후처리: 나쁜 건 거름

(2) 부분 완성 — 빈칸을 채워 보자. 무방비 모델에 무엇을 더해야 시스템이 될까?

시스템 = 모델 + ____ + ____
# 힌트1: 언제 제안할지 정하는 장치
# 힌트2: 나쁜 출력을 거르는 장치

(3) 독립 적용 — 후처리에서 거르고 싶은 나쁜 출력 예를 2가지 직접 적어 보라. (예: 너무 긴 제안, 욕설, 개인정보 노출)


단순 규칙 (이것만 기억하면 됨)

설계가 막히면 이 순서표를 위에서 아래로 채운다.

1. 먼저 묻는다       (속도·규모·안전?)
2. 문제는 다음 토큰   (글 짓기 → 디코더 전용)
3. 데이터 씻고 자르기 (개인정보 가리기 + BPE)
4. 받아서 살짝 더     (다운로드 + 미세 조정 1%)
5. 빔 검색으로 고르기 (후보 몇 갈래 → 제일 좋은 길)
6. 두 잣대로 평가     (시험점수 + 손님반응)
7. 트리거 + 후처리    (모델 혼자선 시스템 아님)

한 칸도 건너뛰지 않으면 빠짐이 없다.


더 해보기


정리

  • 면접은 풀이가 아니라 질문으로 시작한다 — 범위부터 좁힌다.
  • 글 짓기 문제의 정체는 "다음 토큰 예측"이고, 디코더 전용 모델을 고른다.
  • 모델은 부품일 뿐 — 트리거와 후처리까지 묶어야 시스템이다.

다음 1장 예고 — 생성형 AI가 정확히 무엇이고 왜 지금 강력한지, 7단계 설계를 처음부터 끝까지 다시 한번 천천히 본다. (지금은 이 장의 7칸 순서만 들고 가면 충분합니다.)

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플